공통 모달 커스텀 훅 (2)

December 21. 2023

모달이 여러개라면?? [첫번째 난관 !] 🔥

공통모달을 개발하고 다른업무를 보고있었는데, 그 사이에 기획에서 최신 디자인을 반영하였다. 바로 모달창 하나를 띄운상태에서 두번째 모달창을 띄우는 디자인이 생긴 것이다. 하나의 모달을 띄우는 것만 생각하여 props를 객체로 개발했는데 이젠 배열로 변경해야할 때가 온 것이다.
이전에 사용했던 useModal를 다시 재구성 해보자!

const modalState: ModalType[] = [];
const modalAtom = atom(modalState);
 
export const useModal = () => {
    const [modalDataState, setModalDataState] = useAtom(modalAtom);
    const modalOpen = useCallback(
        ({ ...modalState }: ModalType) => {
            setModalDataState((prev) => {
                return [...prev, { isOpen: true, ...modalState }];
            });
        },
        [setModalDataState]
    );
};

ModalOpen부터 보자.
기존에는 객체형태로 props를 받았었는데, 여러개의 모달이 생겨야하므로 modalDataState을 배열로 변경하였다.
그리고, setModalDataState에서 업데이트 하는 부분도 배열로 변경하여 기존배열은을 보존한 채, 새로운 모달을 추가하는 방식이다. 이렇게하면 2개든 3개든 계속 모달데이터가 추가될 것이다.

const initModalDate = () => setModalDataState([]);
 
const modalClose = useCallback(
    (deleteKey?: DeleteKeyType) => {
        if (!deleteKey || modalDataState.length === 1) {
            initModalDate();
        } else {
            setModalDataState((prev) => prev.filter((modal: ModalType) => modal.deleteKey !== deleteKey));
        }
    },
 
    [modalDataState, setModalDataState]
);

여기서 modalClose를 보면 기존과 달리 props에 뭔가가 하나 추가된 것을 알 수 있다. deleteKey가 추가되었다. 기존에는 close함수를 호출하면 모달데이터를 빈 객체로 반환하여 초기화시켰는데, 이번엔 배열이기 때문에 여러개의 모달중 클릭한 모달한 제거해야한다. 그러기 위해서는 모달 각자 고유한 id를 생성하여 제고하자하는 모달 id와 비교하여 필터하면 될거라 생각하여 위와같이 구현하였다.

만약, deleteKey가 없으면 모달데이터는 1개이므로, initModalDate함수를 실행하여 빈배열로 반환시켜준다.
그리고, 2개이상일때는 위의설명한 로직으로 구현하였다. 여기서, 다른개발자가 작업할 때, 모달이 2개이상이지만 깜빡하여 deleteKey를 안넣었을경우를 대비하여 방어코드도 작성하였다.

return (
    <>
        {modalDataState &&
            modalDataState.map((modal: ModalType, index: number) => (
                <Dialog
                    key={index}
                    classes={{ paper: `${styles.modal_box}` }}
                    fullWidth
                    open={modal.isOpen || false}
                    onClose={() => modalClose(modal?.deleteKey)}
                >
                    <IconButton
                        className={styles.modal_close}
                        aria-label="close"
                        onClick={() => modalClose(modal?.deleteKey)}
                    >
                        <Icons.CloseIcon width={24} height={24} fill="#C6CDDA" />
                    </IconButton>
                    <DialogTitle className={`${styles[modal.linetitle ? 'line' : '']}`}>
                        {modal.title}
                        {modal.textTitle && <p className={styles.text_title}>{modal.textTitle}</p>}
                    </DialogTitle>
                    <DialogContent className={`${styles['modal_cont']}`}>{modal.content}</DialogContent>
                </Dialog>
            ))}
    </>
);

모달컴포넌트도 기존에 객체형태였지만, 배열로 형태이므로 map을 돌려서 구성하였다. 각 모달의 deleteKey를 modalClose의 인자로 넘겨서 키를 이용하여 제거하도록 한다.

Container Component

첫번째 모달 적용하기

스크린샷 2023-12-26 오후 4.37.10

export const MutilModalCase = () => {
    const { modalOpen, modalClose } = useModal();
 
    const modalState: ModalType = {
        title: '폼 타입 팝업',
        textTitle: '2번 팝업 내 폼요소가 들어갈 경우에 사용합니다.!!!!!',
        maxWidth: 'm',
        content: <Content modalClose={modalClose} modalOpen={modalOpen} />,
    };
 
    return (
        <>
            <ButtonMui onClick={() => modalOpen(modalState)}>button</ButtonMui>
        </>
    );
};

먼저, 첫번째 모달은 deleteKey가 필요없다. 어차피, 모달이 1개라면 빈 배열로 반환되기 때문이다.
버튼을 클릭하면 모달이 오픈되고, Content 컴포넌트가 호출된다.

const Content = ({ modalOpen, modalClose }) => {
    const modalState: ModalType = {
        title: '폼 타입 팝업',
        textTitle: '팝업 내 폼요소가 들어갈 경우에 사용합니다.',
        maxWidth: 'm',
        deleteKey: 1,
        content: <Content2 setValue={setValue} modalClose={modalClose} />,
    };
    return (
        <>
            <p className="title1 mb_20">나는 타이틀입니다.</p>
            {value}
            ------생략 ----
            <div className="align center mt_30">
                <ButtonMui type="outlined" size="xlarge" onClick={() => modalClose()}>
                    취소
                </ButtonMui>
                <ButtonMui size="xlarge" onClick={() => modalOpen(modalState)}>
                    버튼
                </ButtonMui>
            </div>
        </>
    );
};
 
export default Content;

Content 컴포넌트를 살펴보자. modalState에서 deleteKey를 1로 할당하고 있다. 이 모달의 고유한 아이디값은 1이된다.
즉, 두번째 모달의 Content2의 삭제 키값은 1로 지정하였다.

두번째 모달 적용하기

스크린샷 2023-12-26 오후 4.44.36

const Content2 = ({ setValue, modalClose }) => {
    return (
        <>
            <p>content1에서 설정한 컴포넌트!</p>
            <BaseInput onChange={(e) => setValue(e.target.value)} />
            <ButtonMui type="outlined" size="xlarge" onClick={() => modalClose(1)}>
                취소
            </ButtonMui>
        </>
    );
};

두번째 모달인 Content2 컴포넌트에서 취소버튼을 누를때 modalClose의 인자값을 1로 넘겨주고 있는 모습을 볼 수 있다. 취소가 되면 해당 키값이 제거되고 기존에 있었던 첫번째 모달만 남게된다.